package org.cirdles.topsoil.app.tab;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import org.cirdles.commons.util.ResourceExtractor;
import org.cirdles.topsoil.app.MainWindow;
import org.cirdles.topsoil.app.plot.variable.format.VariableFormat;
import org.cirdles.topsoil.app.plot.variable.format.VariableFormats;
import org.cirdles.topsoil.app.table.TopsoilDataTable;
import org.cirdles.topsoil.app.table.TopsoilTableCell;
import org.cirdles.topsoil.app.table.command.DeleteRowCommand;
import org.cirdles.topsoil.app.table.command.InsertRowCommand;
import org.cirdles.topsoil.app.dataset.entry.TopsoilDataEntry;
import org.cirdles.topsoil.app.plot.PlotPropertiesPanelController;
import java.io.IOException;
import java.util.*;
/**
* This is the primary view for a {@link TopsoilDataTable}. It contains the {@link TableView} that the data is loaded
* into, the {@link PlotPropertiesPanelController} that controls the attributes of any plots for the data, and any
* other visual controls in the {@link TopsoilTab}
*
* @author Jake Marotta
* @see PlotPropertiesPanelController
* @see Tab
* @see TopsoilTabPane
*/
public class TopsoilTabContent extends SplitPane {
//***********************
// Attributes
//***********************
/**
* A {@code GridPane} that contains the {@code Label}s and {@code ChoiceBox}es above the {@code TableView}.
*/
@FXML private GridPane labelGridPane;
/**
* A {@code Label} denoting a {@code TableView} column position as the 'X' column.
*/
@FXML private Label xLabel;
/**
* A {@code Label} denoting a {@code TableView} column position as the 'Y' column.
*/
@FXML private Label yLabel;
/**
* A {@code Label} denoting a {@code TableView} column position as the 'σX' column.
*/
@FXML private Label xUncertaintyLabel;
/**
* A {@code Label} denoting a {@code TableView} column position as the 'σY' column.
*/
@FXML private Label yUncertaintyLabel;
/**
* A {@code Label} denoting a {@code TableView} column position as the 'Corr Coef' column.
*/
@FXML private Label corrCoefLabel;
/**
* A {@code ChoiceBox} for selecting the X Uncertainty {@link VariableFormat}.
*/
@FXML private ChoiceBox<String> xUncertaintyChoiceBox;
/**
* A {@code ChoiceBox} for selecting the Y Uncertainty {@link VariableFormat}.
*/
@FXML private ChoiceBox<String> yUncertaintyChoiceBox;
/**
* A {@code TableView} that displays the table data.
*/
@FXML private TableView<TopsoilDataEntry> tableView;
/**
* A copy of the data contained in the corresponding {@link TopsoilDataTable}.
*/
private ObservableList<TopsoilDataEntry> data;
/**
* A {@code Button} that, when pressed, adds an empty row at the end of the {@code TableView}.
*/
@FXML private Button addRowButton;
/**
* A {@code Button} that, when pressed, removes a row at the end of the {@code TableView}.
*/
@FXML private Button removeRowButton;
/**
* An {@code AnchorPane} that contains the {@link PlotPropertiesPanelController} for this tab.
*/
@FXML private AnchorPane plotPropertiesAnchorPane;
/**
* The {@code PlotPropertiesPanelController} for this tab.
*/
private PlotPropertiesPanelController plotPropertiesPanelController;
/**
* The {@code String} path to the {@code .fxml} file for the {@link PlotPropertiesPanelController}.
*/
private final String PROPERTIES_PANEL_FXML_PATH = "plot-properties-panel.fxml";
/**
* A {@code Map} of {@code String}s to {@code VariableFormat}s, used for getting values from the two uncertainty
* {@code ChoiceBox}es.
*/
private static Map<String, VariableFormat<Number>> STRING_TO_VARIABLE_FORMAT_MAP;
/**
* A {@code Map} of {@code VariableFormat}s to {@code String}s, used for selecting values in the two uncertainty
* {@code ChoiceBox}es.
*/
private static Map<VariableFormat<Number>, String> VARIABLE_FORMAT_TO_STRING_MAP;
static {
// Map VariableFormat names to the formats for displaying and selecting from the uncertainty ChoiceBoxes.
STRING_TO_VARIABLE_FORMAT_MAP = new LinkedHashMap<>();
VARIABLE_FORMAT_TO_STRING_MAP = new LinkedHashMap<>();
for (VariableFormat<Number> format : VariableFormats.UNCERTAINTY_FORMATS) {
STRING_TO_VARIABLE_FORMAT_MAP.put(format.getName(), format);
VARIABLE_FORMAT_TO_STRING_MAP.put(format, format.getName());
}
}
/**
* A {@code ResourceExtractor} for extracting necessary resources. Used by CIRDLES projects.
*/
private final ResourceExtractor RESOURCE_EXTRACTOR = new ResourceExtractor(TopsoilTabContent.class);
//***********************
// Methods
//***********************
/** {@inheritDoc}
*/
@FXML public void initialize() {
assert labelGridPane != null : "fx:id=\"labelGridPane\" was not injected: check your FXML file " +
"'topsoil-tab.fxml'.";
assert xLabel != null : "fx:id=\"xLabel\" was not injected: check your FXML file 'topsoil-tab.fxml'.";
assert yLabel != null : "fx:id=\"yLabel\" was not injected: check your FXML file 'topsoil-tab.fxml'.";
assert xUncertaintyLabel != null : "fx:id=\"xUncertaintyLabel\" was not injected: check your FXML file " +
"'topsoil-tab.fxml'.";
assert yUncertaintyLabel != null : "fx:id=\"yUncertaintyLabel\" was not injected: check your FXML file " +
"'topsoil-tab.fxml'.";
assert corrCoefLabel != null : "fx:id=\"corrCoefLabel\" was not injected: check your FXML file " +
"'topsoil-tab.fxml'.";
assert xUncertaintyChoiceBox != null : "fx:id=\"xUncertaintyChoiceBox\" was not injected: check your FXML " +
"file 'topsoil-tab.fxml'.";
assert yUncertaintyChoiceBox != null : "fx:id=\"yUncertaintyChoiceBox\" was not injected: check your FXML " +
"file 'topsoil-tab.fxml'.";
assert tableView != null : "fx:id=\"tableView\" was not injected: check your FXML file 'topsoil-tab.fxml'.";
assert addRowButton != null : "fx:id=\"addRowButton\" was not injected: check your FXML file 'topsoil-tab.fxml'.";
// Add VariableFormats to uncertainty ChoiceBoxes and select TWO_SIGMA_PERCENT by default
xUncertaintyChoiceBox.setItems(FXCollections.observableArrayList(STRING_TO_VARIABLE_FORMAT_MAP.keySet()));
xUncertaintyChoiceBox.getSelectionModel().select(VariableFormats.TWO_SIGMA_PERCENT.getName());
yUncertaintyChoiceBox.setItems(FXCollections.observableArrayList(STRING_TO_VARIABLE_FORMAT_MAP.keySet()));
yUncertaintyChoiceBox.getSelectionModel().select(VariableFormats.TWO_SIGMA_PERCENT.getName());
// Handle Keyboard Events
tableView.setOnKeyPressed(keyEvent -> handleTableViewKeyEvent(keyEvent));
// Set initial state of remove button.
tableView.itemsProperty().addListener(c -> {
if (tableView.getItems() != null) {
tableView.getItems().addListener((ListChangeListener<TopsoilDataEntry>) d -> {
if (tableView.getItems().isEmpty()) {
removeRowButton.setDisable(true);
} else {
removeRowButton.setDisable(false);
}
});
}
});
configureColumns();
resetIds();
initializePlotPropertiesPanel();
}
/**
* Loads and initializes the {@code PlotPropertiesPanelController} from FXML.
*/
private void initializePlotPropertiesPanel() {
try {
FXMLLoader fxmlLoader = new FXMLLoader(RESOURCE_EXTRACTOR.extractResourceAsPath(PROPERTIES_PANEL_FXML_PATH).toUri().toURL());
Node panel = fxmlLoader.load();
plotPropertiesPanelController = fxmlLoader.getController();
AnchorPane.setTopAnchor(panel, 0.0);
AnchorPane.setRightAnchor(panel, 0.0);
AnchorPane.setBottomAnchor(panel, 0.0);
AnchorPane.setLeftAnchor(panel, 0.0);
plotPropertiesAnchorPane.getChildren().add(panel);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Handles keyboard events in the {@code TableView}.
*
* @param keyEvent a KeyEvent
*/
private void handleTableViewKeyEvent(KeyEvent keyEvent) {
List<TableColumn<TopsoilDataEntry, ?>> columns = tableView.getColumns();
TableView.TableViewSelectionModel<TopsoilDataEntry> selectionModel = tableView.getSelectionModel();
// Tab focuses right cell
// Shift + Tab focuses left cell
if (keyEvent.getCode().equals(KeyCode.TAB)) {
if (keyEvent.isShiftDown()) {
if (selectionModel.getSelectedCells().get(0).getColumn() == 0 &&
selectionModel.getSelectedIndex() != 0) {
selectionModel.select(selectionModel.getSelectedIndex() - 1, this.tableView.getColumns().get
(columns.size() - 1));
} else {
selectionModel.selectLeftCell();
}
} else {
if (selectionModel.getSelectedCells().get(0).getColumn() ==
columns.size() - 1 && selectionModel.getSelectedIndex() != tableView.getItems().size() - 1) {
selectionModel.select(selectionModel.getSelectedIndex() + 1, this.tableView.getColumns().get(0));
} else {
selectionModel.selectRightCell();
}
}
keyEvent.consume();
// Enter moves down or creates new empty row
// Shift + Enter moved up a row
} else if (keyEvent.getCode().equals(KeyCode.ENTER)) {
if (keyEvent.isShiftDown()) {
selectionModel.selectAboveCell();
} else {
// if on last row
if (selectionModel.getSelectedIndex() == tableView.getItems().size() - 1) {
InsertRowCommand insertRowCommand = new InsertRowCommand(this.tableView);
insertRowCommand.execute();
((TopsoilTabPane) this.tableView.getScene().lookup("#TopsoilTabPane")).getSelectedTab()
.addUndo(insertRowCommand);
}
selectionModel.selectBelowCell();
}
keyEvent.consume();
}
}
/**
* Configures the {@code TableColumn}s in the {@code TableView}.
*/
private void configureColumns() {
List<TableColumn<TopsoilDataEntry, ?>> columns = tableView.getColumns();
TableColumn<TopsoilDataEntry, Double> newColumn;
for (int i = 0; i < columns.size(); i++) {
final int columnIndex = i;
newColumn = new TableColumn<>(columns.get(i).getText());
// override cell value factory to accept the i'th index of a data entry for the i'th column
newColumn.setCellValueFactory(param -> {
if (param.getValue().getProperties().size() == 0) {
return (ObservableValue) new SimpleDoubleProperty(0.0);
} else {
// If data was incomplete i.e. length of line is too short for number of columns.
if (param.getValue().getProperties().size() < columnIndex + 1) {
SimpleDoubleProperty newProperty = new SimpleDoubleProperty(Double.NaN);
param.getValue().getProperties().add(newProperty);
return (ObservableValue) newProperty;
} else {
return (ObservableValue) param.getValue().getProperties().get(columnIndex);
}
}
});
// override cell factory to custom editable cells
newColumn.setCellFactory(value -> new TopsoilTableCell());
// disable column sorting
newColumn.setSortable(false);
// newColumn.setId(Integer.toString(i + 1));
// add functional column to the array of columns
columns.set(i, newColumn);
}
}
/**
* Returns the {@code TableView} from this {@code TopsoilTabContent}.
*
* @return TableView
*/
public TableView<TopsoilDataEntry> getTableView() {
return tableView;
}
/**
* Returns the {@code PlotPropertiesPanelController} from this {@code TopsoilTabContent}.
*
* @return PlotPropertiesPanelController
*/
public PlotPropertiesPanelController getPlotPropertiesPanelController() {
return plotPropertiesPanelController;
}
/**
* Sets the data to be displayed.
*
* @param dataEntries an ObservableList of TopsoilDataEntries
*/
public void setData(ObservableList<TopsoilDataEntry> dataEntries) {
this.data = dataEntries;
tableView.setItems(data);
}
/**
* Returns the X Uncertainty {@code VariableFormat} as selected in xUncertaintyChoiceBox.
*
* @return the selected VariableFormat
*/
public VariableFormat<Number> getXUncertainty() {
return STRING_TO_VARIABLE_FORMAT_MAP.get(xUncertaintyChoiceBox.getValue());
}
/**
* Sets the selected X Uncertainty {@code VariableFormat} in the xUncertaintyChoiceBox.
*
* @param format the VariableFormat to select
*/
public void setXUncertainty(VariableFormat<Number> format) {
String o = VARIABLE_FORMAT_TO_STRING_MAP.get(format);
// Find and select the specific item that matches the format.
for (String s : xUncertaintyChoiceBox.getItems()) {
if (s.equals(o)) {
xUncertaintyChoiceBox.getSelectionModel().select(VARIABLE_FORMAT_TO_STRING_MAP.get(format));
break;
}
}
}
/**
* Returns the Y Uncertainty {@code VariableFormat} as selected in yUncertaintyChoiceBox.
*
* @return the selected VariableFormat
*/
public VariableFormat<Number> getYUncertainty() {
return STRING_TO_VARIABLE_FORMAT_MAP.get(yUncertaintyChoiceBox.getValue());
}
/**
* Sets the selected X Uncertainty {@code VariableFormat} in the xUncertaintyChoiceBox.
*
* @param format the VariableFormat to select
*/
public void setYUncertainty(VariableFormat<Number> format) {
String o = VARIABLE_FORMAT_TO_STRING_MAP.get(format);
// Find and select the specific item that matches the format.
for (String s : yUncertaintyChoiceBox.getItems()) {
if (s.equals(o)) {
yUncertaintyChoiceBox.getSelectionModel().select(VARIABLE_FORMAT_TO_STRING_MAP.get(format));
break;
}
}
}
/**
* Resets the {@code String} ids associated with each {@code TableColumn} in the {@code TableView}.
* <p>Each {@code TableColumn} has an associated String id assigned to it, increasing numerically from 1, left to
* right. This is to keep track of the order of the columns before and after they are re-ordered due to clicking and
* dragging.
* </p>
*/
public void resetIds() {
int id = 0;
for (TableColumn<TopsoilDataEntry, ?> column : this.tableView.getColumns()) {
column.setId(Integer.toString(id));
id++;
}
}
/**
* Appends an empty {@code TopsoilDataEntry} to the end of the {@code TableView}.
*/
@FXML private void addRowButtonAction() {
if (tableView.getItems() != null) {
InsertRowCommand insertRowCommand = new InsertRowCommand(tableView);
insertRowCommand.execute();
((TopsoilTabPane) this.tableView.getScene().lookup("#TopsoilTabPane")).getSelectedTab().addUndo
(insertRowCommand);
}
}
/**
* Removes a {@code TopsoilDataEntry} from the end of the {@code TableView}.
*/
@FXML private void removeRowButtonAction() {
if (tableView.getItems() != null && !tableView.getItems().isEmpty()) {
DeleteRowCommand deleteRowCommand = new DeleteRowCommand(tableView);
deleteRowCommand.execute();
((TopsoilTabPane) this.tableView.getScene().lookup("#TopsoilTabPane")).getSelectedTab().addUndo
(deleteRowCommand);
}
}
}